<template>
  <div ref="gridLayoutContainer" class="vue-grid-layout" :style="style">
    <slot></slot>
    <grid-item class="vue-grid-placeholder"
               v-show="isDragging"
               :x="placeholder.x"
               :y="placeholder.y"
               :w="placeholder.w"
               :h="placeholder.h"
               :id="placeholder.id"
    ></grid-item>
  </div>
</template>
<script>
import interact from 'interactjs'
import Emitter from 'tiny-emitter'

import {
  bottom,
  cloneLayout,
  compact,
  getAllCollisions,
  getLayoutItem,
  moveLayoutItem,
  // Throttle,
  validateLayout
} from './helpers/utils'

import {
  findOrGenerateResponsiveLayout,
  getBreakpointFromWidth,
  getColsFromBreakpoint
} from './helpers/responsiveUtils'
import GridItem from './GridItem.vue'
import { addWindowEventListener, removeWindowEventListener } from './helpers/DOM'
import throttle from 'lodash.throttle'

// eslint-disable-next-line @typescript-eslint/no-var-requires
const elementResizeDetectorMaker = require('element-resize-detector')

export default {
  name: 'GridLayout',
  provide () {
    return {
      eventBus: this.eventBus,
      gridLayout: this
    }
  },
  components: {
    GridItem
  },
  props: {
    // If true, the container height swells and contracts to fit contents
    autoSize: {
      type: Boolean,
      default: true
    },
    colNum: {
      type: Number,
      default: 12
    },
    rowHeight: {
      type: Number,
      default: 150
    },
    maxRows: {
      type: Number,
      default: Infinity
    },
    // [X] support minRows
    minRows: {
      type: Number,
      default: 1
    },
    margin: {
      type: Array,
      default: function () {
        return [10, 10]
      }
    },
    isDraggable: {
      type: Boolean,
      default: true
    },
    isResizable: {
      type: Boolean,
      default: true
    },
    isMirrored: {
      type: Boolean,
      default: false
    },
    useCssTransforms: {
      type: Boolean,
      default: true
    },
    verticalCompact: {
      type: Boolean,
      default: true
    },
    layout: {
      type: Array,
      required: true
    },
    responsive: {
      type: Boolean,
      default: false
    },
    responsiveLayouts: {
      type: Object,
      default: function () {
        return {}
      }
    },
    breakpoints: {
      type: Object,
      default: function () {
        return {
          lg: 1200,
          md: 996,
          sm: 768,
          xs: 480,
          xxs: 0
        }
      }
    },
    cols: {
      type: Object,
      default: function () {
        return {
          lg: 12,
          md: 10,
          sm: 6,
          xs: 4,
          xxs: 2
        }
      }
    },
    preventCollision: {
      type: Boolean,
      default: true
    },
    useStyleCursor: {
      type: Boolean,
      default: true
    },
    itemHover: {
      type: Boolean,
      default: false
    },
    freezeTime: {
      type: Number,
      default: 0
    }
  },
  data: function () {
    return {
      width: null,
      style: {},
      lastLayoutLength: 0,
      isDragging: false,
      placeholder: {
        x: 0,
        y: 0,
        w: 0,
        h: 0,
        id: -1
      },
      layouts: {}, // array to store all layouts from different breakpoints
      lastBreakpoint: null, // store last active breakpoint
      originalLayout: null, // store original Layout
      coverLayoutItem: null,
      freezeThrottle: null
    }
  },
  beforeCreate () {
    this.eventBus = new Emitter()
  },
  created () {
    // Accessible references of functions for removing in beforeUnmount
    this.resizeEventHandler = (eventType, id, x, y, h, w) => {
      // console.log('resizeEventHandler', eventType, id, x, y, h, w)
      this.resizeEvent(eventType, id, x, y, h, w)
    }

    this.dragEventHandler = (event) => {
      this.dragEvent(event)
    }

    this.eventBus.on('resizeEvent', this.resizeEventHandler)
    this.eventBus.on('dragEvent', this.dragEventHandler)
    this.freezeThrottleMoveAndCompact = throttle(this.moveAndCompact, this.freezeTime)

    this.$emit('layout-created', this.layout)
  },
  beforeUnmount: function () {
    // Remove listeners
    this.eventBus.off('resizeEvent', this.resizeEventHandler)
    this.eventBus.off('dragEvent', this.dragEventHandler)
    this.freezeThrottleMoveAndCompact.cancel()

    removeWindowEventListener('resize', this.onWindowResize)
    if (this.erd) {
      this.erd.uninstall(this.$refs.gridLayoutContainer)
    }
  },
  beforeMount: function () {
    this.$emit('layout-before-mount', this.layout)
  },
  mounted: function () {
    this.$emit('layout-mounted', this.layout)
    this.$nextTick(() => {
      validateLayout(this.layout)
      this.originalLayout = this.layout
      this.$nextTick(() => {
        // init container
        this.initResponsiveFeatures()
        addWindowEventListener('resize', this.onWindowResize)
        this.$nextTick(() => {
          this.onWindowResize()
        })
        // initialize layout with constraint
        compact(this.layout, this.verticalCompact)
        // this.$emit('layout-updated', this.layout)
        this.updateHeight()
        // dropzone
        this.interactInstance = interact(this.$refs.gridLayoutContainer)
        this.interactInstance.dropzone({
          // only accept elements matching this CSS selector
          accept: '.app-small-card-grid-item',
          // Require a 75% element overlap for a drop to be possible
          overlap: 0.75,
          ondropactivate: event => {
            this.$emit('drop-active', event)
          },
          ondragenter: event => {
            this.$emit('drag-enter', event)
          },
          ondragleave: event => {
            this.$emit('drag-leave', event)
          },
          ondrop: event => {
            this.$emit('drop', event)
          },
          ondropdeactivate: event => {
            this.$emit('drop-deactivate', event)
          }
        })
        // windows resize detector
        this.$nextTick(() => {
          this.erd = elementResizeDetectorMaker(Object.assign({
            strategy: 'scroll',
            callOnAdd: false
          }))
          this.erd.listenTo(this.$refs.gridLayoutContainer, () => {
            this.onWindowResize()
          })
        })
      })
    })
  },
  watch: {
    width: function (newValue, oldValue) {
      this.$nextTick(() => {
        this.eventBus.emit('updateWidth', this.width)
        if (oldValue === null) {
          this.$nextTick(() => {
            this.$emit('layout-ready', this.layout)
          })
        }
        this.updateHeight()
      })
    },
    layout: function () {
      if (this.layout !== undefined && this.originalLayout !== null) {
        if (this.layout.length !== this.originalLayout.length) {
          // console.log('### LAYOUT UPDATE!', this.layout.length, this.originalLayout.length);
          const diff = this.findDifference(this.layout, this.originalLayout)
          // [?] if delete one item and append one item
          if (diff.length > 0) {
            // console.log(diff);
            if (this.layout.length > this.originalLayout.length) {
              this.originalLayout = this.originalLayout.concat(diff)
            } else {
              this.originalLayout = this.originalLayout.filter(obj => {
                return !diff.some(obj2 => {
                  return obj.id === obj2.id
                })
              })
            }
          }
          this.lastLayoutLength = this.layout.length
          this.initResponsiveFeatures()
        }
        compact(this.layout, this.verticalCompact)
        this.eventBus.emit('updateWidth', this.width)
        this.updateHeight()
        // [X] trigger by watch.layout will cause loop error
        // this.$emit('layout-updated', this.layout)
      }
    },
    colNum: function (val) {
      this.eventBus.emit('setColNum', val)
    },
    rowHeight: function () {
      this.eventBus.emit('setRowHeight', this.rowHeight)
    },
    isDraggable: function () {
      this.eventBus.emit('setDraggable', this.isDraggable)
    },
    isResizable: function () {
      this.eventBus.emit('setResizable', this.isResizable)
    },
    responsive () {
      if (!this.responsive) {
        this.$emit('update:layout', this.originalLayout)
        this.eventBus.emit('setColNum', this.colNum)
      }
      this.onWindowResize()
    },
    maxRows: function () {
      this.eventBus.emit('setMaxRows', this.maxRows)
    },
    margin () {
      this.updateHeight()
    }
  },
  methods: {
    updateHeight: function () {
      this.style = {
        height: this.containerHeight()
      }
    },
    onWindowResize: function () {
      // console.log('onWindowResize', this.$refs.gridLayoutContainer.offsetWidth);
      if (this.$refs !== null && this.$refs.gridLayoutContainer !== null && this.$refs.gridLayoutContainer !== undefined) {
        this.width = this.$refs.gridLayoutContainer.offsetWidth
      }
      this.eventBus.emit('resizeEvent')
    },
    containerHeight: function () {
      if (!this.autoSize) return
      const currentHighest = bottom(this.layout) * (this.rowHeight + this.margin[1]) + this.margin[1]
      const minHeight = this.minRows * (this.rowHeight + this.margin[1]) + this.margin[1]
      // console.log('bottom: ' + bottom(this.layout))
      // console.log('rowHeight + margins: ' + (this.rowHeight + this.margin[1]) + this.margin[1])
      return Math.max(currentHighest, minHeight) + 'px'
    },
    dragEvent: function (event) {
      const eventName = event.type
      const newPosition = event.newPosition
      const gridSize = event.gridSize
      const updatingLayout = event.updatingLayout
      const id = updatingLayout.id
      const x = updatingLayout.x
      const y = updatingLayout.y
      const h = updatingLayout.h
      const w = updatingLayout.w

      // console.log(eventName + ' id=' + id + ', x=' + x + ', y=' + y);
      let layoutItem = getLayoutItem(this.layout, id)
      // GetLayoutItem sometimes returns null object
      if (layoutItem === undefined || layoutItem === null) {
        layoutItem = {
          x: 0,
          y: 0
        }
      }

      if (eventName === 'dragmove' || eventName === 'dragstart') {
        this.placeholder.id = id
        this.placeholder.x = layoutItem.x
        this.placeholder.y = layoutItem.y
        this.placeholder.w = w
        this.placeholder.h = h
        this.$nextTick(function () {
          this.isDragging = true
        })
        this.eventBus.emit('updateWidth', this.width)
      } else {
        this.$nextTick(function () {
          this.isDragging = false
        })
      }
      // [X] Add cover feature
      // [?] Custom code in common components
      // Check item hover status
      // console.log('newPosition ', JSON.stringify(newPosition));
      // console.log('gridSize ', JSON.stringify(gridSize));
      const needCover = !!~id.indexOf('APP_')
      const coverLayoutItem = needCover && this.itemHover && this.layout.find(node => {
        // const cellWidth = this.calcColWidth();
        if (node.id === updatingLayout.id) return false
        const area = updatingLayout
        const top = newPosition.top
        const left = newPosition.left
        const cellWidth = gridSize.cellWidth
        const cellHeight = gridSize.cellHeight
        // calc ...
        const nodeLeft = node.x * cellWidth
        const nodeTop = node.y * cellHeight
        const nodeRight = nodeLeft + node.w * cellWidth
        const nodeBottom = nodeTop + node.h * cellHeight
        const nodeCenterX = (nodeLeft + nodeRight) / 2
        const nodeCenterY = (nodeTop + nodeBottom) / 2
        const areaCenterX = left + area.w * cellWidth / 2
        const areaCenterY = top + area.h * cellHeight / 2
        const coverSpaceX = node.w * cellWidth / 2 / 1.1
        const coverSpaceY = node.h * cellHeight / 2 / 1.1
        return Math.abs(areaCenterX - nodeCenterX) < coverSpaceX &&
            Math.abs(areaCenterY - nodeCenterY) < coverSpaceY
      })
      this.coverLayoutItem = coverLayoutItem
      // coverLayoutItem && console.log('coverLayoutItem ', coverLayoutItem.id);

      if (!coverLayoutItem) {
        // console.log('freezeThrottleMoveAndCompact');
        if (this.freezeTime && needCover) {
          this.freezeThrottleMoveAndCompact(layoutItem, x, y)
        } else {
          this.moveAndCompact(layoutItem, x, y)
        }
      }

      // Update DOM style
      this.eventBus.emit('updateGridItemStyle')
      this.updateHeight()

      // emit layout update
      if (eventName === 'dragend') {
        coverLayoutItem && console.log('dragend coverLayoutItem: ', coverLayoutItem.id)
        if (coverLayoutItem) {
          this.$emit('DragendCover', {
            updatingItem: updatingLayout,
            coverLayoutItem: coverLayoutItem
          })
          this.$nextTick(() => {
            this.coverLayoutItem = null
            this.eventBus.emit('updateGridItemStyle')
          })
        } else {
          this.$emit('layout-updated', this.layout)
        }
      }
    },
    moveAndCompact (layoutItem, x, y) {
      // Move the element to the dragged location.
      // eslint-disable-next-line vue/no-mutating-props
      // console.log('moveAndCompact', layoutItem, x, y);
      // console.log('Before moveAndCompact', JSON.stringify(this.layout));
      moveLayoutItem(this.layout, layoutItem,
          {
            x,
            y
          },
          {
            isUserAction: true,
            preventCollision: this.preventCollision
          }
      )
      // needed because vue can't detect changes on array element properties
      compact(this.layout, this.verticalCompact)
      // console.log('After moveAndCompact', JSON.stringify(this.layout));
    },
    resizeEvent: function (eventName, id, x, y, h, w) {
      let resizeLayoutItem = getLayoutItem(this.layout, id)
      // GetLayoutItem sometimes return null object
      if (!resizeLayoutItem) {
        resizeLayoutItem = {
          w: 0,
          h: 0
        }
      }
      let hasCollisions
      if (this.preventCollision) {
        const collisions = getAllCollisions(this.layout, {
          x: resizeLayoutItem.x,
          y: resizeLayoutItem.y,
          w,
          h
        }).filter(
            layoutItem => layoutItem.id !== resizeLayoutItem.id
        )
        hasCollisions = collisions.length > 0
        // If we're colliding, we need adjust the placeholder.
        if (hasCollisions) {
          // adjust w && h to maximum allowed space
          let leastX = Infinity
          let leastY = Infinity
          collisions.forEach(layoutItem => {
            if (layoutItem.x > resizeLayoutItem.x) leastX = Math.min(leastX, layoutItem.x)
            if (layoutItem.y > resizeLayoutItem.y) leastY = Math.min(leastY, layoutItem.y)
          })
          if (Number.isFinite(leastX)) resizeLayoutItem.w = leastX - resizeLayoutItem.x
          if (Number.isFinite(leastY)) resizeLayoutItem.h = leastY - resizeLayoutItem.y
        }
      }

      if (!hasCollisions) {
        // Set new width and height.
        resizeLayoutItem.w = w
        resizeLayoutItem.h = h
      }

      if (eventName === 'resizestart' || eventName === 'resizemove') {
        this.placeholder.id = id
        this.placeholder.x = x
        this.placeholder.y = y
        this.placeholder.w = resizeLayoutItem.w
        this.placeholder.h = resizeLayoutItem.h
        this.$nextTick(function () {
          this.isDragging = true
        })
        this.eventBus.emit('updateWidth', this.width)
      } else {
        this.$nextTick(function () {
          this.isDragging = false
        })
      }

      if (this.responsive) this.responsiveGridLayout()

      compact(this.layout, this.verticalCompact)
      this.eventBus.emit('updateGridItemStyle')
      this.updateHeight()

      if (eventName === 'resizeend') this.$emit('layout-updated', this.layout)
    },

    // finds or generates new layouts for set breakpoints
    responsiveGridLayout () {
      const newBreakpoint = getBreakpointFromWidth(this.breakpoints, this.width)
      const newCols = getColsFromBreakpoint(newBreakpoint, this.cols)

      // submit actual layout in layouts
      if (this.lastBreakpoint != null && !this.layouts[this.lastBreakpoint]) {
        this.layouts[this.lastBreakpoint] = cloneLayout(this.layout)
      }

      // Find or generate a new layout.
      const layout = findOrGenerateResponsiveLayout(
          this.originalLayout,
          this.layouts,
          this.breakpoints,
          newBreakpoint,
          this.lastBreakpoint,
          newCols,
          this.verticalCompact
      )

      // Store the new layout.
      this.layouts[newBreakpoint] = layout

      if (this.lastBreakpoint !== newBreakpoint) {
        this.$emit('breakpoint-changed', newBreakpoint, layout)
      }

      // new prop sync
      this.$emit('update:layout', layout)

      this.lastBreakpoint = newBreakpoint
      this.eventBus.emit('setColNum', getColsFromBreakpoint(newBreakpoint, this.cols))
    },

    // clear all responsive layouts
    initResponsiveFeatures () {
      // clear layouts
      this.layouts = Object.assign({}, this.responsiveLayouts)
    },

    // find difference in layouts
    findDifference (layout, originalLayout) {
      // Find values that are in result1 but not in result2
      const uniqueResultOne = layout.filter(function (obj) {
        return !originalLayout.some(function (obj2) {
          return obj.id === obj2.id
        })
      })

      // Find values that are in result2 but not in result1
      const uniqueResultTwo = originalLayout.filter(function (obj) {
        return !layout.some(function (obj2) {
          return obj.id === obj2.id
        })
      })

      // Combine the two arrays of unique entries#
      return uniqueResultOne.concat(uniqueResultTwo)
    }
  }
}
</script>
<style>
.vue-grid-layout {
  position: relative;
  transition: height 200ms ease;
}
</style>
